查看原文
其他

90% 的 Java 程序员都会犯的错

程序员DHL ByteCode 2023-12-27

hi 这是 dhl 的第 86 篇文章

个人微信: hi-dhl

Hi 大家好,我是 DHL,大厂全栈程序员,就职于 美团、快手、小米。分享技术干货和编程知识点,包含性能优化、系统源码、算法、数据结构、大厂面经

视频版




文字版


前一段时间在刷 LeetCode 多线程相关题目的时候,看到使用 ReentrantLock 有两种不同的写法。

方式一:Oracle 官方推荐的写法

private val look = ReentrantLock()
fun printNumber() {
   look.lock()
   try {
       // TODO
  } finally {
       look.unlock()
  }
}

方式二:错误的写法

private val look = ReentrantLock()
fun printNumber() {
   try {
       look.lock()
  } finally {
       look.unlock()
  }
}

方法一 是 Oracle 推荐的方式,并且在 「阿里巴巴JAVA开发手册」 明确规定了不建议使用 方式二,即不建议将 lock.lock() 写在 try...finally 代码块内部,一起来分析都有哪些需要注意的细节。

通过这篇文章,将会学习到以下内容:

  • lock() 方法为什么不能放在 try...finally 代码块内部?

  • lock() 方法放在 try...finally 代码块内部第一行安全吗?

  • 为什么要在 finally 代码块中执行 unlock() 方法?

  • lock() 方法放在 try 代码块外部一定安全吗?

Lock () 方法为什么不能放在 try... Finally 代码块内部

避免由于其他代码段抛出异常,造成加锁失败,导致在 finally 代码块中调用 unlock() 解锁方法,对未加锁的对象进行解锁,从而抛出 IllegalMonitorStateException 异常(依赖具体的实现), unlock()  源码很简单,如下所示。java/util/concurrent/locks/ReentrantLock. Java

public void unlock() {
   sync.release(1);
}

unlock 方法被委派到了 Sync 类上,Sync 继承自 AbstractQueuedSynchronizerjava/util/concurrent/locks/AbstractQueuedSynchronizer. Java

public final boolean release(int arg) {
   if (tryRelease(arg)) {
       // ......
       return true;
  }
   return false;
}

// 供子类重写
protected boolean tryRelease(int arg) {
   throw new UnsupportedOperationException();
}

子类 ReentrantLockReentrantReadWriteLock 都会重写 tryRelease 方法。这里主要看一下 ReentrantLock#tryRelease 方法。java/util/concurrent/locks/ReentrantLock. Java

protected final boolean tryRelease(int releases) {
   if (Thread.currentThread() != getExclusiveOwnerThread())
       throw new IllegalMonitorStateException();
   // ......
   return free;
}

tryRelease 方法中,会判断当前线程是否等于拥有锁的线程,如果不相等则表示加锁失败,会抛出 IllegalMonitorStateException 异常。同时也会造成真正的异常信息被覆盖掉,代码如下所示。

fun printNumber() {
   try {
       val number = 1 / 0
       look.lock()
       // TODO
  } finally {
       look.unlock()
  }
}

正如代码所示,希望出现的异常信息应该是 java.lang.ArithmeticException: / by zero,但是实际运行的时候,异常信息如下所示,真正的异常信息被覆盖掉了。

为什么要在 finally 代码块中执行 unlock () 方法

既然 unlock() 方法会抛出异常,为什么还要将它放在 finally 代码块中,这是为了保证执行过程中出现异常,依然能够保证锁会被释放掉,避免死锁。

注意:需要将 unlock() 方法放到 finally 代码块第一行。

Lock () 方法写在 try... Finally 代码块内部第一行安全吗

我在网上看到部分回答,说可以将 lock() 方法写在 try...finally 代码块内部第一行,即  lock() 方法前不会添加其他代码,但是这样真的安全吗?不一定,只能说出现问题的概率很低,一起来看一下源码描述。

根据 lock() 方法的描述,可能抛出 unchecked 异常(依赖具体的实现), 如果放在 try...finally 代码块内部,必然会触发 finally 代码块中 unlock() 方法。在 unlock() 方法中会检查是否持有锁,未持有锁则会抛出 IllegalMonitorStateException 异常(依赖具体的实现)。

根据 unlock() 方法的描述,通常只有锁的持有者才能释放锁,也就是说当非锁持有线程调用 unlock() 方法时会抛出 unchecked 异常,虽然两个方法都是因为加锁失败导致的,但是真正的异常信息会被 unlock() 方法抛出的异常信息覆盖掉。

Lock () 方法写在 try 代码块外部一定安全吗

既然 unlock() 方法会抛出异常,为什么还要将它放在 finally 代码块中,这是为了保证执行过程中出现异常,依然能够保证锁会被释放掉,避免死锁。

注意:需要将 unlock() 方法放到 finally 代码块第一行。

Lock () 方法写在 try... Finally 代码块内部第一行安全吗

我在网上看到部分回答,说可以将 lock() 方法写在 try...finally 代码块内部第一行,即  lock() 方法前不会添加其他代码,但是这样真的安全吗?不一定,只能说出现问题的概率很低,一起来看一下源码描述。

根据 lock() 方法的描述,可能抛出 unchecked 异常(依赖具体的实现), 如果放在 try...finally 代码块内部,必然会触发 finally 代码块中 unlock() 方法。在 unlock() 方法中会检查是否持有锁,未持有锁则会抛出 IllegalMonitorStateException 异常(依赖具体的实现)。

根据 unlock() 方法的描述,通常只有锁的持有者才能释放锁,也就是说当非锁持有线程调用 unlock() 方法时会抛出 unchecked 异常,虽然两个方法都是因为加锁失败导致的,但是真正的异常信息会被 unlock() 方法抛出的异常信息覆盖掉。

Lock () 方法写在 try 代码块外部一定安全吗

lock() 方法放在 try 代码块外部一定安全吗?不一定,取决于我们的代码是如何实现的,异常代码如下所示。

fun printNumber() {
   look.lock()
   // 抛出异常的代码
   try {
       // ......
  } finally {
       // 最后保证锁会被释放掉
       look.unlock()
  }
}

在加锁方法 lock()try 代码块之间抛出了异常,那么就会出现加锁成功,但是无法解锁,会造成其他线程无法获取锁。

如何避免以上的问题的发生

在使用 ReentrantLock 获取锁的时候,需要注意以下几点:

  • look() 方法必须写在 try 代码块之外

  • look() 方法和  try...finally 代码块之间,没有其他的代码段,避免出现无法解锁,造成其他线程无法获取到锁

  • unlock() 要放到 finally 代码块第一行


全文到这里就结束了,感谢你的阅读,坚持原创不易,欢迎 在看点赞分享 给身边的小伙伴,我会持续分享原创干货!!!


推荐阅读

适配 Android 14,功能和权限的变更,你的应用受影响了吗

被裁现状,给找工作的同学一些建议
要失业了,AI 帮我完成了一个需求



Hi 大家好,我是 DHL,就职于 美团、快手、小米。分享技术干货和编程知识点,包含性能优化、系统源码、算法、数据结构、大厂面经。

  • 哔哩哔哩:https://space.bilibili.com/498153238

  • 掘金:https://juejin.im/user/2594503168898744

  • 博客:https://hi-dhl.com

  • Github:https://github.com/hi-dhl

👇🏻 真诚推荐你关注我👇🏻

因微信公众号更改了推送机制

可能无法及时看到最新文章

 将公众号设为 星标 

或常为文章点 在看

即可及时收到最新文章


欢迎前往 博客 查看更多 Kotlin、Jetpack 、动画算法图解、系统源码分析等等文章。以及开源项目、LeetCode / 剑指 offer / 国内外大厂面试题 / 多线程 题解。

https://www.hi-dhl.com

继续滑动看下一个

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存